﻿#region License
/*
    Copyright (c) 2010, Paweł Hofman (CodeTitans)
    All Rights Reserved.

    Licensed under the Apache License version 2.0.
    For more information please visit:

    http://codetitans.codeplex.com/license
        or
    http://www.apache.org/licenses/


    For latest source code, documentation, samples
    and more information please visit:

    http://codetitans.codeplex.com/
*/
#endregion

using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.IO;
using System.Globalization;
using System.Diagnostics;
using System.Threading;

namespace CodeTitans.Bayeux
{
    /// <summary>
    /// Class responsible for transmitting data for designated web servers over HTTP protocol.
    /// </summary>
    public class HttpConnection
    {
        public const string MethodPost = "POST";
        public const string MethodGet = "GET";

        private const string HeaderSentAt = "X-Sent";

        private readonly string _url;
        private readonly Encoding _defaultEncoding;
        private HttpWebRequest _request;
        private Dictionary<string, string> _headers;

        #region Events

        /// <summary>
        /// Event fired if data has been received for current request.
        /// </summary>
        public event EventHandler<HttpConnectionEventArgs> DataReceived;
        /// <summary>
        /// Event fired if data reception failed for current request.
        /// </summary>
        public event EventHandler<HttpConnectionEventArgs> DataReceiveFailed;

        #endregion

        /// <summary>
        /// Init constructor.
        /// </summary>
        public HttpConnection(string url, Encoding defaultEncoding, string contentType)
        {
            _url = url;
            _defaultEncoding = defaultEncoding ?? Encoding.UTF8;
            ContentType = contentType;
            Timeout = 20 * 1000;
        }

        /// <summary>
        /// Init constructor.
        /// </summary>
        public HttpConnection(string url)
            : this(url, null, null)
        {
        }

        public void SendRequest(string data, string method, bool asynchronous)
        {
            SendRequest(null, data, method, asynchronous);
        }

        public void SendRequest(string relativeUrlPath, string data, string method, bool asynchronous)
        {
            if (_request != null)
                throw new InvalidOperationException("Another request is being processed. Call Cancel() method first.");
            if (method != MethodPost && method != MethodGet)
                throw new InvalidOperationException("Invalid method used. Try 'POST' or 'GET'.");

            //////////////////////
            // FILL HTTP request:
            string uri = string.IsNullOrEmpty(relativeUrlPath) ? _url : _url + relativeUrlPath;
            HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(uri);
            DateTime now = DateTime.Now;

            if (NetworkCredential != null)
            {
#if !PocketPC
                webRequest.UseDefaultCredentials = false;
#endif
                webRequest.Credentials = NetworkCredential;
            }

#if !PocketPC && !WINDOWS_PHONE
            webRequest.CachePolicy = new System.Net.Cache.HttpRequestCachePolicy(System.Net.Cache.HttpRequestCacheLevel.NoCacheNoStore);
            webRequest.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
#endif
            webRequest.Method = method;
            webRequest.Accept = AcceptContentType;
            webRequest.ContentType = ContentType;
            webRequest.UserAgent = UserAgent;

#if WINDOWS_PHONE
            webRequest.Headers[HeaderSentAt] = now.Ticks.ToString();
            webRequest.AllowAutoRedirect = false;
            webRequest.AllowReadStreamBuffering = false;
#else
            webRequest.Headers.Add(HeaderSentAt, now.Ticks.ToString());
            webRequest.KeepAlive = false;
            webRequest.Timeout = Timeout;
            webRequest.Pipelined = false;
#endif
            _request = webRequest;

            // append additional headers:
            if (_headers != null)
            {
#if WINDOWS_PHONE
                foreach (KeyValuePair<string, string> header in _headers)
                    webRequest.Headers[header.Key] = header.Value;

#else
                foreach (KeyValuePair<string, string> header in _headers)
                    webRequest.Headers.Add(header.Key, header.Value);
#endif
            }

            AppendAdditionalInfo(webRequest);

            //////////////////////
            // Format data to sent:
            if (data != null)
            {
                try
                {
                    using (Stream requestStream = GetRequestStream(webRequest))
                    {
                        WriteRequestData(_defaultEncoding.GetBytes(data), requestStream);
                    }
                }
                catch (WebException ex)
                {
                    Event.Invoke(DataReceiveFailed, this, new HttpConnectionEventArgs(this, HttpStatusCode.RequestTimeout, ex.Message));
                    _request = null;
                    return;
                }
            }

            WriteLine(string.Format(CultureInfo.InvariantCulture, "<--- Sending request (length: {0} bytes, at: {1}): {2}",
                                        data != null ? data.Length : 0, now, uri));
            WriteLine(data ?? string.Empty);

            //////////////////////
            // Send request:
            if (asynchronous)
            {
                webRequest.BeginGetResponse (AsyncWebResponseCallback, webRequest);
            }
            else
            {
                HttpWebResponse response = null;

                try
                {
                    response = GetResponse(webRequest);
                }
                catch (WebException ex)
                {
                    response = (HttpWebResponse)ex.Response;
                }

                try
                {
                    ProcessResponse(webRequest, response);
                }
                finally
                {
                }
            }
        }

        /// <summary>
        /// Gets the response object in synchronous way.
        /// </summary>
        private HttpWebResponse GetResponse(HttpWebRequest request)
        {
#if WINDOWS_PHONE
            responseReceived = false;
            IAsyncResult asyncResult = request.BeginGetResponse(AsyncPhoneWebResponseCallback, request);

            // HACK: this is a fake behaviour, as Windows Phone 7 doesn't support synchronous web requests,
            // that's why the current thread must poll for result:
            while (!responseReceived)
            {
                Thread.Sleep(50);
            }

            return (HttpWebResponse)request.EndGetResponse(asyncResult);
#else
            return (HttpWebResponse)request.GetResponse();
#endif
        }

        /// <summary>
        /// Gets the request stream in synchronous way.
        /// </summary>
        private Stream GetRequestStream(HttpWebRequest request)
        {
#if WINDOWS_PHONE
            requestStreamReceived = false;
            IAsyncResult asyncResult = request.BeginGetRequestStream(AsyncPhoneWebRequestStreamCallback, request);

            // HACK: take a look on comment of GetResponse() method...
            while (!requestStreamReceived)
            {
                Thread.Sleep(50);
            }

            return request.EndGetRequestStream(asyncResult);
#else
            return request.GetRequestStream();
#endif
        }

#if WINDOWS_PHONE

        private volatile bool responseReceived;
        private volatile bool requestStreamReceived;

        private void AsyncPhoneWebResponseCallback(IAsyncResult result)
        {
            // update indication that response is received:
            responseReceived = true;
        }

        private void AsyncPhoneWebRequestStreamCallback(IAsyncResult result)
        {
            // update indication that request stream is available to write:
            requestStreamReceived = true;
        }

#endif

        private static string GetResponseEncoding(HttpWebResponse response)
        {
#if WINDOWS_PHONE
            return response.Headers[HttpRequestHeader.ContentEncoding];
#else
            return response.ContentEncoding;
#endif
        }

        public void Cancel()
        {
            if (_request == null)
                throw new InvalidOperationException("No operation to cancel");

            WriteLine("Aborting HTTP request!");
            _request.Abort();
            _request = null;
        }

        public bool IsActive
        {
            get { return _request != null; }
        }

        public string ContentType
        {
            get;
            set;
        }

        public string AcceptContentType
        {
            get { return ContentType; }
        }

        public NetworkCredential NetworkCredential
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the timeout for next request (ms).
        /// </summary>
        public int Timeout
        {
            get; set;
        }

        public string UserAgent
        {
            get; set;
        }

        /// <summary>
        /// Adds or replaces additional header sent via this connection with next request.
        /// </summary>
        public void AddHeader(string name, string value)
        {
            if (_headers == null)
                _headers = new Dictionary<string, string>();

            if (_headers.ContainsKey(name))
                _headers[name] = value;
            else
                _headers.Add(name, value);
        }

        /// <summary>
        /// Removes header with specified name.
        /// </summary>
        public void RemoveHeader(string name)
        {
            if (_headers != null)
            {
                _headers.Remove(name);
            }
        }

        private void AsyncWebResponseCallback(IAsyncResult asyncResult)
        {
            HttpWebRequest request = (HttpWebRequest)asyncResult.AsyncState;
            HttpWebResponse response = null;

            try
            {
                response = (HttpWebResponse)request.EndGetResponse(asyncResult);
            }
            catch (WebException ex)
            {
                response = (HttpWebResponse)ex.Response;
            }

            try
            {
                ProcessResponse(request, response);
            }
            finally
            {
            }
        }

        /// <summary>
        /// Add new extensions to HttpWebRequest before sending to the server.
        /// </summary>
        protected virtual void AppendAdditionalInfo(HttpWebRequest request)
        {
        }

        protected void ProcessResponse(HttpWebRequest request, HttpWebResponse response)
        {
            // release current request:
            _request = null;

            // data reception failed or timeouted/cancelled?
            if (response == null)
            {
                WriteLine(string.Format(CultureInfo.InvariantCulture, "---> Response critical failure! Probably timedout or cancelled (at: {0}).", DateTime.Now));
                Event.Invoke(DataReceiveFailed, this, new HttpConnectionEventArgs(this, HttpStatusCode.ServiceUnavailable, null));
                return;
            }

            // and process the data:
            HttpStatusCode responseStatusCode = response.StatusCode;
            string responseStatusDescription = response.StatusDescription;
            DateTime now = DateTime.Now;
            DateTime sentAt = new DateTime(long.Parse(request.Headers[HeaderSentAt]));
            long contentLength = response.ContentLength;

            if (contentLength < 0 && response.Headers["Content-Length"] != null)
            {
                try
                {
                    contentLength = long.Parse(response.Headers["Content-Length"]);
                }
                catch
                {
                }
            }

            WriteLine(string.Format("---> Received response (length: {0} bytes, at: {1}, waiting: {2:F2} sec) with status: {3} ({4}, {5})", contentLength, now, (now - sentAt).TotalSeconds, response.StatusCode, (int)response.StatusCode, response.StatusDescription));

            if (responseStatusCode != HttpStatusCode.OK && responseStatusCode != HttpStatusCode.Created && responseStatusCode != HttpStatusCode.Accepted)
            {
                response.Close();
                Event.Invoke(DataReceiveFailed, this, new HttpConnectionEventArgs(this, responseStatusCode, responseStatusDescription));
                return;
            }

            string data;

            try
            {
                using (Stream responseStream = response.GetResponseStream())
                {
                    string encodingName = GetResponseEncoding(response);
                    data = ReadResponseData(responseStream, contentLength, (string.IsNullOrEmpty(encodingName) ? _defaultEncoding : Encoding.GetEncoding(encodingName)));
                }

                response.Close();
            }
            catch (Exception ex)
            {
                WriteLine(string.Format("Response parsing error: {0}", ex.Message));
                Event.Invoke(DataReceiveFailed, this, new HttpConnectionEventArgs(this, responseStatusCode, responseStatusDescription));
                return;
            }

            WriteLine(data ?? string.Empty);
            Event.Invoke(DataReceived, this, new HttpConnectionEventArgs(this, responseStatusCode, responseStatusDescription, data));
        }

        [Conditional("DEBUG")]
        private static void WriteLine(string message)
        {
            Debug.WriteLine(message != null && message.Length > 1024 ? message.Substring(1024) : message);
            Console.WriteLine(message);
        }

        /// <summary>
        /// Reads data from given source.
        /// </summary>
        protected string ReadToEnd(StreamReader reader, long maxLength)
        {
#if WINDOWS_PHONE
            // HACK: Some network streams on Windows Phone 7 have 'seek-support' non-implemented issue.
            // To fix it, try to read data in chunks:

            StringBuilder result = new StringBuilder((int)maxLength);
            char[] buffer = new char[4 * 1024];
            long totalRead = 0;
            int read;

            do
            {
                read = reader.Read(buffer, 0, buffer.Length);

                if (read == 0)
                    break;

                totalRead += read;
                result.Append(buffer, 0, read);
            }
            while (totalRead < maxLength);

            return result.ToString();
#else
            return reader.ReadToEnd();
#endif
        }

        #region Virtual Methods

        /// <summary>
        /// Writes data to the server as part of the request stream.
        /// </summary>
        protected virtual void WriteRequestData(byte[] data, Stream output)
        {
            output.Write(data, 0, data.Length);
        }

        /// <summary>
        /// Reads data received from server and interprets according to given encoding.
        /// </summary>
        protected virtual string ReadResponseData(Stream input, long contentLength, Encoding encoding)
        {
            if (input == null || !input.CanRead)
                return null;

            using (StreamReader reader = new StreamReader(input, encoding))
            {
                return ReadToEnd(reader, contentLength);
            }
        }

        #endregion
    }
}
